【第1085期】React作者的构思和演绎
前言
周末了。今日早读文章由58同城前端架构师@ramroll翻译分享。
正文从这开始~
这是React作者在React设计之初,对整个框架的思考
我通过这篇文章试图阐述我对React模型的理解,阐述我们是如何用【演绎推导】来帮助我们得到最后的设计。
当然,这里有很多的前置条件是有争议的,而且这篇文章中的例子是有缺陷和漏洞。 但这是我们正式地去规范化它。如果你有更好的想法去形式化它,请随意向我们发PR。即便在没有太多库文件和细节的情况下,从简单到复杂的演绎应该是有意义的。
最终的React实现版本,需要大量务实的解决方案、增量迭代、算法优化、遗留代码、调试工具——这些如果有实用价值,都更新太快。所以react的实现过程是很难推导的。
所以我想给大家展示一个简单的构思模型让我可以立足其中。
Transformation(变换)
React的一个核心假设就是UI是数据到试图的映射。同样的输入总是可以得到同样的输出。
function NameBox(name) {
return { fontWeight: 'bold', labelContent: name };
}
'Sebastian Markbåge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markbåge' };
抽象
你不可以在单个函数中使用复杂的UI。你需要先把界面抽象成为可以复用的部分,而且每个部分不泄露自己的实现细节(作为一个函数)。这样你就可以从一个函数调用另一个函数。
function FancyUserBox(user) {
return {
borderStyle: '1px solid blue',
childContent: [
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]
};
}
{ firstName: 'Sebastian', lastName: 'Markbåge' } ->
{
borderStyle: '1px solid blue',
childContent: [
'Name: ',
{ fontWeight: 'bold', labelContent: 'Sebastian Markbåge' }
]
};
上述代码FancyUserBox()和NameBox() 嵌套
Composition(组合)
为了实现真正可重用的特性,仅仅重用叶子节点,并在叶子节点上构造新的容器是不够的。 你需要在抽象的容器下面构造一个抽象容器的组合。 所谓的组合,就是将多个抽象合并成为一个新的抽象。
function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}
function UserBox(user) {
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}
其实这里说的组合就是多个组件(数组)可以作为一个整体渲染;这里说的抽象,其实就是组件。
State(状态)
UI不是简单地复制服务端的业务逻辑和状态,有很多状态是针对一个特殊的投射。比如在文本框中输入文字不会投射到其他tab或者设备上。 滚动位置不会在多个投射间复用。(小编:投射应该就是状态流过组件函数,最终被渲染的过程。输入框中被输入的文字和滚动位置这些状态太特殊了,是针对一种特殊的投射,没有办法从服务端复用,所以才需要状态)。
我们倾向于选择的数据模型是不可变的(immutable),我们将函数串联起来,并且认为更新状态的函数在最顶端。
function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}
// Implementation Details
var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}
// Init
FancyNameBox(
{ firstName: 'Sebastian', lastName: 'Markbåge' },
likes,
addOneMoreLike
);
这个例子在更新状态的时候有副作用。我真实的构思是在更新过程中函数返回下一个状态。这里是用最简单的方式给大家阐述原理,以后我们会更新这个例子。
Memoization(记忆)
调用一个纯函数一遍又一遍调用它其实非常浪费性能。我们可以把这个计算过程的输入和结果缓存起来。这样就不用重复计算。
function memoize(fn) {
var cachedArg;
var cachedResult;
return function(arg) {
if (cachedArg === arg) {
return cachedResult;
}
cachedArg = arg;
cachedResult = fn(arg);
return cachedResult;
};
}
var MemoizedNameBox = memoize(NameBox);
function NameAndAgeBox(user, currentTime) {
return FancyBox([
'Name: ',
MemoizedNameBox(user.firstName + ' ' + user.lastName),
'Age in milliseconds: ',
currentTime - user.dateOfBirth
]);
}
Lists
很多UI都是列表——列表中的每一项有不同的数据,这构成了一种天然的结构。
为了管理每一个元素的状态,我们可以创造一个Map把每个元素的状态存起来。
function UserList(users, likesPerUser, updateUserLikes) {
return users.map(user => FancyNameBox(
user,
likesPerUser.get(user.id),
() => updateUserLikes(user.id, likesPerUser.get(user.id) + 1)
));
}
var likesPerUser = new Map();
function updateUserLikes(id, likeCount) {
likesPerUser.set(id, likeCount);
rerender();
}
UserList(data.users, likesPerUser, updateUserLikes);
我们将多个不同的参数传给了FancyNameBox,这个功能和记忆模块冲突了,因为我们每次只能记住一个值.接下来我们会讨论这点 .
Continuations(连续性)
不幸的是,UI中有太多的列表的列表,最后需要写很多的重复代码(boilerplate code)。
我们可以通过延迟执行函数,把一部分重复代码移出关键业务。例如:我们可以使用柯里化方法(bind in JavaScript)。这样当我们把状态从外部传入核心函数时不会遇到重复代码。
这种方法虽然没有消除重复代码,但至少将重复代码移出了核心业务逻辑。
function FancyUserList(users) {
return FancyBox(
UserList.bind(null, users)
);
}
const box = FancyUserList(data.users);
const resolvedChildren = box.children(likesPerUser, updateUserLikes);
const resolvedBox = {
...box,
children: resolvedChildren
};
State Map(状态映射)
我们很早就知道,如果我们看到重复的模式,我们可以使用组合的方法避免重复实现这个模式。 我们可以把提取和传递状态的逻辑,移动到一个底层的函数中去。
function FancyBoxWithState(
children,
stateMap,
updateState
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState
))
);
}
function UserList(users) {
return users.map(user => {
continuation: FancyNameBox.bind(null, user),
key: user.id
});
}
function FancyUserList(users) {
return FancyBoxWithState.bind(null,
UserList(users)
);
}
const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);
Memoization Map
当我们试图记住列表中的多个项时,记忆变得更加困难。你需要找到一些复杂的缓存算法去平衡内存使用和频率。
庆幸的是,UI在同一位置是相对稳定的。树的相同位置总是得到相同的结果。这让树成为记忆的有效策略。
这样我们可以用同样的技术去缓存组合函数。
function memoize(fn) {
return function(arg, memoizationCache) {
if (memoizationCache.arg === arg) {
return memoizationCache.result;
}
const result = fn(arg);
memoizationCache.arg = arg;
memoizationCache.result = result;
return result;
};
}
function FancyBoxWithState(
children,
stateMap,
updateState,
memoizationCache
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState,
memoizationCache.get(child.key)
))
);
}
const MemoizedFancyNameBox = memoize(FancyNameBox);
Algebraic Effects
React看上去像个PITA饼,一层又一层的,一个很小的值也需要这样传递。 这样我们在不同层之间需要一个短路——"context"
有时候数据依赖并没有很好地遵循抽象树,举个例子,在布局算法中你需要事先知道子元素的的大小。
接下来的这个例子有点超出我们目前讨论的范围。我用Algebraic Effects(类似副作用的一个词汇),ECMAScript提出的来解释。如果你对函数式编程非常熟悉,他们在避免monads实践过程中炸了(they're avoiding the intermediate ceremony imposed by monads)。
function ThemeBorderColorRequest() { }
function FancyBox(children) {
const color = raise new ThemeBorderColorRequest();
return {
borderWidth: '1px',
borderColor: color,
children: children
};
}
function BlueTheme(children) {
return try {
children();
} catch effect ThemeBorderColorRequest -> [, continuation] {
continuation('blue');
}
}
function App(data) {
return BlueTheme(
FancyUserList.bind(null, data.users)
);
}
点评:
大巧不工——被作者这样一说,复杂的东西其实也很简单。
千里之行始于足下,学好简单的知识点,可以搞大事情
关于本文
译者:@ramroll
译文:https://zhuanlan.zhihu.com/p/30277192
原文:https://github.com/reactjs/react-basic/blob/master/README.md